package org.volicy.scheduled.sch;

import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.env.Environment;
import org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.scheduling.config.CronTask;
import org.springframework.scheduling.config.ScheduledTask;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.scheduling.support.ScheduledMethodRunnable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.volicy.scheduled.dao.ScheduledMapper;
import org.volicy.scheduled.param.ScheduledParam;
import org.volicy.scheduled.vo.JobInfoVO;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;

/**
 * @Author: volicy.xu
 * @Date: 2021/9/22
 */
public class ScheduleHandler implements InitializingBean, BeanFactoryAware {
    private static final Logger LOGGER = LoggerFactory.getLogger(ScheduleHandler.class);

    private static final String APP_NAME = "spring.application.name";
    private static final String TASK_NAME = "_JOB_TASK";
    private static final AtomicLong ATOMIC_LONG = new AtomicLong(0L);

    private ScheduledMapper scheduledMapper;

    private BeanFactory beanFactory;

    private ThreadPoolTaskExecutor executor;

    private Set<ScheduledTask> scheduledTasks;

    private ScheduledAnnotationBeanPostProcessor scheduledAnnotationBeanPostProcessor;

    private ScheduledTaskRegistrar scheduledTaskRegistrar;

    private Environment environment;


    public ScheduleHandler(ScheduledMapper scheduledMapper) {
        this.scheduledMapper = scheduledMapper;
    }

    /**
     * @throws NoSuchFieldException
     * @throws IllegalAccessException
     */
    public void init() {
        this.scheduledTasks = this.scheduledAnnotationBeanPostProcessor.getScheduledTasks();
        String appName = environment.getProperty(APP_NAME);
        if (!CollectionUtils.isEmpty(this.scheduledTasks)) {
            this.scheduledTasks.stream().forEach(s -> {
                CronTask cronTask = (CronTask) s.getTask();
                ScheduledMethodRunnable scheduledMethodRunnable = (ScheduledMethodRunnable) cronTask.getRunnable();
                Class<?> clazz = scheduledMethodRunnable.getMethod().getDeclaringClass();//类名
                String name = scheduledMethodRunnable.getMethod().getName();//方法名
                if (scheduledMapper.queryJobById(cronTask.getExpression(), appName) == null) {
                    saveJobInfo(appName, cronTask, clazz, name);
                }
            });
            scheduledMapper.queryJobStatus().forEach(j -> {
                cancel(j.getJobExpress(), j.getJobClassName(), j.getId(), Boolean.FALSE);
            });
        }
    }

    /**
     * @param scheduledParam
     * @return
     */
    public PageInfo<JobInfoVO> findByJobName(ScheduledParam scheduledParam) {
        String jobName = StringUtils.hasText(scheduledParam.getJobName()) ? "%" + scheduledParam.getJobName() + "%" : "";
        PageHelper.startPage(scheduledParam.getPageNo() - 1, scheduledParam.getPageSize());
        return new PageInfo<>(scheduledMapper.queryJob(jobName));
    }

    /**
     * @param beanFactory
     * @throws BeansException
     */
    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
    }


    /**
     * @throws Exception
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        this.executor = this.beanFactory.getBean(ThreadPoolTaskExecutor.class);
        this.environment = this.beanFactory.getBean(Environment.class);
        this.scheduledAnnotationBeanPostProcessor = this.beanFactory.getBean(ScheduledAnnotationBeanPostProcessor.class);
        getScheduledTaskRegistrar();
    }

    /**
     * @param appName
     * @param cronTask
     * @param clazz
     * @param name
     */
    public void saveJobInfo(String appName, CronTask cronTask, Class<?> clazz, String name) {
        JobInfoVO jobInfoVO = new JobInfoVO();
        jobInfoVO.setJobAppName(appName);
        jobInfoVO.setJobExpress(cronTask.getExpression());
        jobInfoVO.setJobClassName(clazz.getName());
        jobInfoVO.setJobName(jobInfoVO.getJobAppName().toUpperCase(Locale.ENGLISH) + TASK_NAME + ATOMIC_LONG.getAndIncrement());
        jobInfoVO.setJobMethodName(name);
        scheduledMapper.insertJob(jobInfoVO);
    }

    /**
     * @return
     * @throws NoSuchFieldException
     * @throws IllegalAccessException
     */
    private ScheduledTaskRegistrar getScheduledTaskRegistrar() throws NoSuchFieldException, IllegalAccessException {
        Field registrar = scheduledAnnotationBeanPostProcessor
                .getClass()
                .getDeclaredField("registrar");
        registrar.setAccessible(true);
        this.scheduledTaskRegistrar = (ScheduledTaskRegistrar) registrar.get(scheduledAnnotationBeanPostProcessor);
        return this.scheduledTaskRegistrar;
    }

    /**
     * @param clazz
     * @throws ClassNotFoundException
     */
    public void resume(String clazz, String methodName, String express, Long id) throws Exception {
        Class<?> aClass = Class.forName(clazz);
        Object bean = beanFactory.getBean(aClass);
        CronTask cronTask = new CronTask(createRunnable(bean, aClass.getDeclaredMethod(methodName)),
                new CronTrigger(express, TimeZone.getDefault()));
        this.scheduledTasks.add(this.scheduledTaskRegistrar.scheduleCronTask(cronTask));
        this.scheduledTaskRegistrar.addCronTask(cronTask);
        scheduledMapper.updateJobStatusById(ScheduledConstant.NORMAL_STATUS, id);
    }

    /**
     * @param target
     * @param method
     * @return
     */
    protected Runnable createRunnable(Object target, Method method) {
        Assert.isTrue(method.getParameterCount() == 0, "Only no-arg methods may be annotated with @Scheduled");
        Method invocableMethod = AopUtils.selectInvocableMethod(method, target.getClass());
        return new ScheduledMethodRunnable(target, invocableMethod);
    }

    /**
     * @param clazz
     * @param methodName
     * @throws Exception
     */
    public void trigger(String clazz, String methodName) throws Exception {
        Class<?> aClass = Class.forName(clazz);
        Method declaredMethod = aClass.getDeclaredMethod(methodName);
        executor.execute(() -> {
            try {
                declaredMethod.invoke(beanFactory.getBean(aClass));
            } catch (Exception ex) {
                LOGGER.error("trigger error", ex);
            }
        });
    }


    /**
     * @param express
     * @param clazz
     * @param id
     */
    public void remove(String express, String clazz, Long id) {
        cancel(express, clazz, id, Boolean.FALSE);
        scheduledMapper.updateJobStatusById(ScheduledConstant.DELETE_STATUS, id);
    }

    /**
     * @param express
     * @param clazz
     */
    public void cancel(String express, String clazz, Long id, boolean isUpdate) {
        Set<ScheduledTask> scheduledTasks0 = new LinkedHashSet<>();
        List<CronTask> cronTaskList = new ArrayList<>(this.scheduledTaskRegistrar.getCronTaskList());
        for (ScheduledTask scheduledTask : scheduledTasks) {
            CronTask cronTask = (CronTask) scheduledTask.getTask();
            ScheduledMethodRunnable scheduledMethodRunnable = (ScheduledMethodRunnable) cronTask.getRunnable();
            if (express.equals(cronTask.getExpression()) &&
                    clazz.equals(scheduledMethodRunnable.getMethod().getDeclaringClass().getName())) {
                scheduledTask.cancel();
                cronTaskList.remove(cronTask);
                scheduledTasks0.add(scheduledTask);
                if (isUpdate)
                    scheduledMapper.updateJobStatusById(ScheduledConstant.CANCEL_STATUS, id);
            }
        }
        this.scheduledTaskRegistrar.setCronTasksList(cronTaskList);
        if (!CollectionUtils.isEmpty(scheduledTasks0)) {
            scheduledTasks.removeAll(scheduledTasks0);
        }
    }
}
